import { SubscriptionDialog } from "../dialogs/subscription-dialog";
import {
  IncomingResponseMessage,
  OutgoingRequestMessage,
  OutgoingSubscribeRequest,
  OutgoingSubscribeRequestDelegate
} from "../messages";
import { SubscriptionState } from "../subscription";
import { Timers } from "../timers";
import { NonInviteClientTransaction } from "../transactions";
import { UserAgentCore } from "../user-agent-core";
import { NotifyUserAgentServer } from "./notify-user-agent-server";
import { UserAgentClient } from "./user-agent-client";

/**
 * SUBSCRIBE UAC.
 * @remarks
 * 4.1.  Subscriber Behavior
 * https://tools.ietf.org/html/rfc6665#section-4.1
 *
 * User agent client for installation of a single subscription per SUBSCRIBE request.
 * TODO: Support for installation of multiple subscriptions on forked SUBSCRIBE requests.
 * @public
 */
export class SubscribeUserAgentClient extends UserAgentClient implements OutgoingSubscribeRequest {
  public delegate: OutgoingSubscribeRequestDelegate | undefined;

  /** Dialog created upon receiving the first NOTIFY. */
  private dialog: SubscriptionDialog | undefined;
  /** Identifier of this user agent client. */
  private subscriberId: string;
  /** When the subscription expires. Starts as requested expires and updated on 200 and NOTIFY. */
  private subscriptionExpires: number;
  /** The requested expires for the subscription. */
  private subscriptionExpiresRequested: number;
  /** Subscription event being targeted. */
  private subscriptionEvent: string;
  /** Subscription state. */
  private subscriptionState: SubscriptionState;
  /** Timer N Id. */
  private N: number | undefined;

  constructor(core: UserAgentCore, message: OutgoingRequestMessage, delegate?: OutgoingSubscribeRequestDelegate) {
    // Get event from request message.
    const event = message.getHeader("Event");
    if (!event) {
      throw new Error("Event undefined");
    }
    // Get expires from request message.
    const expires = message.getHeader("Expires");
    if (!expires) {
      throw new Error("Expires undefined");
    }
    super(NonInviteClientTransaction, core, message, delegate);
    this.delegate = delegate;

    // FIXME: Subscriber id should also be matching on event id.
    this.subscriberId = message.callId + message.fromTag + event;
    this.subscriptionExpiresRequested = this.subscriptionExpires = Number(expires);
    this.subscriptionEvent = event;
    this.subscriptionState = SubscriptionState.NotifyWait;

    // Start waiting for a NOTIFY we can use to create a subscription.
    this.waitNotifyStart();
  }

  /**
   * Destructor.
   * Note that Timer N may live on waiting for an initial NOTIFY and
   * the delegate may still receive that NOTIFY. If you don't want
   * that behavior then either clear the delegate so the delegate
   * doesn't get called (a 200 will be sent in response to the NOTIFY)
   * or call `waitNotifyStop` which will clear Timer N and remove this
   * UAC from the core (a 481 will be sent in response to the NOTIFY).
   */
  public dispose(): void {
    super.dispose();
  }

  /**
   * Handle out of dialog NOTIFY associated with SUBSCRIBE request.
   * This is the first NOTIFY received after the SUBSCRIBE request.
   * @param uas - User agent server handling the subscription creating NOTIFY.
   */
  public onNotify(uas: NotifyUserAgentServer): void {
    // NOTIFY requests are matched to such SUBSCRIBE requests if they
    // contain the same "Call-ID", a "To" header field "tag" parameter that
    // matches the "From" header field "tag" parameter of the SUBSCRIBE
    // request, and the same "Event" header field.  Rules for comparisons of
    // the "Event" header fields are described in Section 8.2.1.
    // https://tools.ietf.org/html/rfc6665#section-4.4.1
    const event: string = uas.message.parseHeader("Event").event;
    if (!event || event !== this.subscriptionEvent) {
      this.logger.warn(`Failed to parse event.`);
      uas.reject({ statusCode: 489 });
      return;
    }

    // NOTIFY requests MUST contain "Subscription-State" header fields that
    // indicate the status of the subscription.
    // https://tools.ietf.org/html/rfc6665#section-4.1.3
    const subscriptionState = uas.message.parseHeader("Subscription-State");
    if (!subscriptionState || !subscriptionState.state) {
      this.logger.warn("Failed to parse subscription state.");
      uas.reject({ statusCode: 489 });
      return;
    }
    // Validate subscription state.
    const state: string = subscriptionState.state;
    switch (state) {
      case "pending":
        break;
      case "active":
        break;
      case "terminated":
        break;
      default:
        this.logger.warn(`Invalid subscription state ${state}`);
        uas.reject({ statusCode: 489 });
        return;
    }

    // Dialogs usages are created upon completion of a NOTIFY transaction
    // for a new subscription, unless the NOTIFY request contains a
    // "Subscription-State" of "terminated."
    // https://tools.ietf.org/html/rfc6665#section-4.4.1
    if (state !== "terminated") {
      // The Contact header field MUST be present and contain exactly one SIP
      // or SIPS URI in any request that can result in the establishment of a
      // dialog.
      // https://tools.ietf.org/html/rfc3261#section-8.1.1.8
      const contact = uas.message.parseHeader("contact");
      if (!contact) {
        this.logger.warn("Failed to parse contact.");
        uas.reject({ statusCode: 489 });
        return;
      }
    }

    // In accordance with the rules for proxying non-INVITE requests as
    // defined in [RFC3261], successful SUBSCRIBE requests will receive only
    // one 200-class response; however, due to forking, the subscription may
    // have been accepted by multiple nodes.  The subscriber MUST therefore
    // be prepared to receive NOTIFY requests with "From:" tags that differ
    // from the "To:" tag received in the SUBSCRIBE 200-class response.
    //
    // If multiple NOTIFY requests are received in different dialogs in
    // response to a single SUBSCRIBE request, each dialog represents a
    // different destination to which the SUBSCRIBE request was forked.
    // Subscriber handling in such situations varies by event package; see
    // Section 5.4.9 for details.
    // https://tools.ietf.org/html/rfc6665#section-4.1.4

    // Each event package MUST specify whether forked SUBSCRIBE requests are
    // allowed to install multiple subscriptions.
    //
    // If such behavior is not allowed, the first potential dialog-
    // establishing message will create a dialog.  All subsequent NOTIFY
    // requests that correspond to the SUBSCRIBE request (i.e., have
    // matching "To", "From", "Call-ID", and "Event" header fields, as well
    // as "From" header field "tag" parameter and "Event" header field "id"
    // parameter) but that do not match the dialog would be rejected with a
    // 481 response.  Note that the 200-class response to the SUBSCRIBE
    // request can arrive after a matching NOTIFY request has been received;
    // such responses might not correlate to the same dialog established by
    // the NOTIFY request.  Except as required to complete the SUBSCRIBE
    // transaction, such non-matching 200-class responses are ignored.
    //
    // If installing of multiple subscriptions by way of a single forked
    // SUBSCRIBE request is allowed, the subscriber establishes a new dialog
    // towards each notifier by returning a 200-class response to each
    // NOTIFY request.  Each dialog is then handled as its own entity and is
    // refreshed independently of the other dialogs.
    //
    // In the case that multiple subscriptions are allowed, the event
    // package MUST specify whether merging of the notifications to form a
    // single state is required, and how such merging is to be performed.
    // Note that it is possible that some event packages may be defined in
    // such a way that each dialog is tied to a mutually exclusive state
    // that is unaffected by the other dialogs; this MUST be clearly stated
    // if it is the case.
    // https://tools.ietf.org/html/rfc6665#section-5.4.9

    // *** NOTE: This implementation is only for event packages which
    // do not allow forked requests to install multiple subscriptions.
    // As such and in accordance with the specification, we stop waiting
    // and any future NOTIFY requests will be rejected with a 481.
    if (this.dialog) {
      throw new Error("Dialog already created. This implementation only supports install of single subscriptions.");
    }
    this.waitNotifyStop();

    // Update expires.
    this.subscriptionExpires = subscriptionState.expires
      ? Math.min(this.subscriptionExpires, Math.max(subscriptionState.expires, 0))
      : this.subscriptionExpires;

    // Update subscription state.
    switch (state) {
      case "pending":
        this.subscriptionState = SubscriptionState.Pending;
        break;
      case "active":
        this.subscriptionState = SubscriptionState.Active;
        break;
      case "terminated":
        this.subscriptionState = SubscriptionState.Terminated;
        break;
      default:
        throw new Error(`Unrecognized state ${state}.`);
    }

    // Dialogs usages are created upon completion of a NOTIFY transaction
    // for a new subscription, unless the NOTIFY request contains a
    // "Subscription-State" of "terminated."
    // https://tools.ietf.org/html/rfc6665#section-4.4.1
    if (this.subscriptionState !== SubscriptionState.Terminated) {
      // Because the dialog usage is established by the NOTIFY request, the
      // route set at the subscriber is taken from the NOTIFY request itself,
      // as opposed to the route set present in the 200-class response to the
      // SUBSCRIBE request.
      // https://tools.ietf.org/html/rfc6665#section-4.4.1
      const dialogState = SubscriptionDialog.initialDialogStateForSubscription(this.message, uas.message);

      // Subscription Initiated! :)
      this.dialog = new SubscriptionDialog(
        this.subscriptionEvent,
        this.subscriptionExpires,
        this.subscriptionState,
        this.core,
        dialogState
      );
    }

    // Delegate.
    if (this.delegate && this.delegate.onNotify) {
      const request = uas;
      const subscription = this.dialog;
      this.delegate.onNotify({ request, subscription });
    } else {
      uas.accept();
    }
  }

  public waitNotifyStart(): void {
    if (!this.N) {
      // Add ourselves to the core's subscriber map.
      // This allows the core to route out of dialog NOTIFY messages to us.
      this.core.subscribers.set(this.subscriberId, this);
      this.N = setTimeout(() => this.timerN(), Timers.TIMER_N);
    }
  }

  public waitNotifyStop(): void {
    if (this.N) {
      // Remove ourselves to the core's subscriber map.
      // Any future out of dialog NOTIFY messages will be rejected with a 481.
      this.core.subscribers.delete(this.subscriberId);
      clearTimeout(this.N);
      this.N = undefined;
    }
  }

  /**
   * Receive a response from the transaction layer.
   * @param message - Incoming response message.
   */
  protected receiveResponse(message: IncomingResponseMessage): void {
    if (!this.authenticationGuard(message)) {
      return;
    }

    if (message.statusCode && message.statusCode >= 200 && message.statusCode < 300) {
      //  The "Expires" header field in a 200-class response to SUBSCRIBE
      //  request indicates the actual duration for which the subscription will
      //  remain active (unless refreshed).  The received value might be
      //  smaller than the value indicated in the SUBSCRIBE request but cannot
      //  be larger; see Section 4.2.1 for details.
      // https://tools.ietf.org/html/rfc6665#section-4.1.2.1

      // The "Expires" values present in SUBSCRIBE 200-class responses behave
      // in the same way as they do in REGISTER responses: the server MAY
      // shorten the interval but MUST NOT lengthen it.
      //
      //    If the duration specified in a SUBSCRIBE request is unacceptably
      //    short, the notifier may be able to send a 423 response, as
      //    described earlier in this section.
      //
      // 200-class responses to SUBSCRIBE requests will not generally contain
      // any useful information beyond subscription duration; their primary
      // purpose is to serve as a reliability mechanism.  State information
      // will be communicated via a subsequent NOTIFY request from the
      // notifier.
      // https://tools.ietf.org/html/rfc6665#section-4.2.1.1
      const expires = message.getHeader("Expires");
      if (!expires) {
        this.logger.warn("Expires header missing in a 200-class response to SUBSCRIBE");
      } else {
        const subscriptionExpiresReceived = Number(expires);
        if (subscriptionExpiresReceived > this.subscriptionExpiresRequested) {
          this.logger.warn(
            "Expires header in a 200-class response to SUBSCRIBE with a higher value than the one in the request"
          );
        }
        if (subscriptionExpiresReceived < this.subscriptionExpires) {
          this.subscriptionExpires = subscriptionExpiresReceived;
        }
      }
      // If a NOTIFY arrived before 200-class response a dialog may have been created.
      // Updated the dialogs expiration only if this indicates earlier expiration.
      if (this.dialog) {
        if (this.dialog.subscriptionExpires > this.subscriptionExpires) {
          this.dialog.subscriptionExpires = this.subscriptionExpires;
        }
      }
    }

    if (message.statusCode && message.statusCode >= 300 && message.statusCode < 700) {
      this.waitNotifyStop(); // No NOTIFY will be sent after a negative final response.
    }

    super.receiveResponse(message);
  }

  /**
   * To ensure that subscribers do not wait indefinitely for a
   * subscription to be established, a subscriber starts a Timer N, set to
   * 64*T1, when it sends a SUBSCRIBE request.  If this Timer N expires
   * prior to the receipt of a NOTIFY request, the subscriber considers
   * the subscription failed, and cleans up any state associated with the
   * subscription attempt.
   * https://tools.ietf.org/html/rfc6665#section-4.1.2.4
   */
  private timerN(): void {
    this.logger.warn(`Timer N expired for SUBSCRIBE user agent client. Timed out waiting for NOTIFY.`);
    this.waitNotifyStop();
    if (this.delegate && this.delegate.onNotifyTimeout) {
      this.delegate.onNotifyTimeout();
    }
  }
}
